# Copyright 2018 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Toolchain for compiling rust stubs from protobuf and gRPC."""

load("@rules_proto//proto:proto_common.bzl", proto_toolchains = "toolchains")

# buildifier: disable=bzl-visibility
load("//rust/private:utils.bzl", "name_to_crate_name")

def generated_file_stem(file_path):
    """Returns the basename of a file without any extensions.

    Example:
    ```python
    content.append("pub mod %s;" % _generated_file_stem(f))
    ```

    Args:
        file_path (string): A path to a file

    Returns:
        string: The file stem of the filename
    """
    basename = file_path.rsplit("/", 2)[-1]
    basename = name_to_crate_name(basename)
    return basename.rsplit(".", 2)[0]

def rust_generate_proto(
        ctx,
        transitive_descriptor_sets,
        protos,
        imports,
        output_dir,
        proto_toolchain,
        is_grpc = False):
    """Generate a proto compilation action.

    Args:
        ctx (ctx): rule context.
        transitive_descriptor_sets (depset): descriptor generated by previous protobuf libraries.
        protos (list): list of paths of protos to compile.
        imports (depset): directory, relative to the package, to output the list of stubs.
        output_dir (str): The basename of the output directory for for the output generated stubs
        proto_toolchain (ToolchainInfo): The toolchain for rust-proto compilation. See `rust_proto_toolchain`
        is_grpc (bool, optional): generate gRPC stubs. Defaults to False.

    Returns:
        list: the list of generate stubs (File)
    """

    tools = [
        proto_toolchain.protoc,
        proto_toolchain.proto_plugin,
    ]
    executable = proto_toolchain.protoc
    args = ctx.actions.args()

    if not protos:
        fail("Protobuf compilation requested without inputs!")
    paths = ["%s/%s" % (output_dir, generated_file_stem(i)) for i in protos.to_list()]
    outs = [ctx.actions.declare_file(path + ".rs") for path in paths]
    output_directory = outs[0].dirname

    # Throughout we use rules_rust as the name as the plugin, not rust, because rust is an unstable builtin language in protoc.
    # If we use rust as the plugin name, it triggers protoc to try to use its in-built support, which is experimental.
    # The naming here doesn't matter, it's arbitrary, just the plugin name and the out dir need to match, so we pick rules_rust.

    if is_grpc:
        # Add grpc stubs to the list of outputs
        grpc_files = [ctx.actions.declare_file(path + "_grpc.rs") for path in paths]
        outs.extend(grpc_files)

        # gRPC stubs is generated only if a service is defined in the proto,
        # so we create an empty grpc module in the other case.
        tools.append(proto_toolchain.grpc_plugin)
        tools.append(ctx.executable._optional_output_wrapper)
        args.add_all(grpc_files)
        args.add_all([
            "--",
            proto_toolchain.protoc,
            "--plugin=protoc-gen-grpc-rules_rust=" + proto_toolchain.grpc_plugin.path,
            "--grpc-rules_rust_out=" + output_directory,
        ])
        executable = ctx.executable._optional_output_wrapper

    args.add_all([
        "--plugin=protoc-gen-rules_rust=" + proto_toolchain.proto_plugin.path,
        "--rules_rust_out=" + output_directory,
    ])

    args.add_joined(
        transitive_descriptor_sets,
        join_with = ":",
        format_joined = "--descriptor_set_in=%s",
    )

    args.add_all(protos)
    ctx.actions.run(
        inputs = depset(
            transitive = [
                transitive_descriptor_sets,
                imports,
            ],
        ),
        outputs = outs,
        tools = tools,
        progress_message = "Generating Rust protobuf stubs",
        mnemonic = "RustProtocGen",
        executable = executable,
        arguments = [args],
    )
    return outs

def _rust_proto_toolchain_impl(ctx):
    if ctx.attr.protoc:
        # buildifier: disable=print
        print("WARN: rust_prost_toolchain's proto_compiler attribute is deprecated. Make sure your rules_proto dependency is at least version 6.0.0 and stop setting proto_compiler")

    proto_toolchain = proto_toolchains.find_toolchain(
        ctx,
        legacy_attr = "_legacy_proto_toolchain",
        toolchain_type = "@rules_proto//proto:toolchain_type",
    )

    return platform_common.ToolchainInfo(
        edition = ctx.attr.edition,
        grpc_compile_deps = ctx.attr.grpc_compile_deps,
        grpc_plugin = ctx.attr.protoc or ctx.file.grpc_plugin,
        proto_compile_deps = ctx.attr.proto_compile_deps,
        proto_plugin = ctx.file.proto_plugin,
        protoc = ctx.executable.protoc or proto_toolchain.proto_compiler,
    )

# Default dependencies needed to compile protobuf stubs.
PROTO_COMPILE_DEPS = [
    Label("//proto/protobuf/3rdparty/crates:protobuf"),
]

# Default dependencies needed to compile gRPC stubs.
GRPC_COMPILE_DEPS = PROTO_COMPILE_DEPS + [
    Label("//proto/protobuf/3rdparty/crates:grpc"),
    Label("//proto/protobuf/3rdparty/crates:tls-api"),
    Label("//proto/protobuf/3rdparty/crates:tls-api-stub"),
]

rust_proto_toolchain = rule(
    implementation = _rust_proto_toolchain_impl,
    attrs = dict({
        "edition": attr.string(
            doc = "The edition used by the generated rust source.",
        ),
        "grpc_compile_deps": attr.label_list(
            doc = "The crates the generated grpc libraries depends on.",
            cfg = "target",
            default = GRPC_COMPILE_DEPS,
        ),
        "grpc_plugin": attr.label(
            doc = "The location of the Rust protobuf compiler plugin to generate rust gRPC stubs.",
            allow_single_file = True,
            cfg = "exec",
            default = Label("//proto/protobuf/3rdparty/crates:grpc-compiler__protoc-gen-rust-grpc"),
        ),
        "proto_compile_deps": attr.label_list(
            doc = "The crates the generated protobuf libraries depends on.",
            cfg = "target",
            default = PROTO_COMPILE_DEPS,
        ),
        "proto_plugin": attr.label(
            doc = "The location of the Rust protobuf compiler plugin used to generate rust sources.",
            allow_single_file = True,
            cfg = "exec",
            default = Label("//proto/protobuf/3rdparty/crates:protobuf-codegen__protoc-gen-rust"),
        ),
        "protoc": attr.label(
            doc = "The location of the `protoc` binary. It should be an executable target. Note that this attribute is deprecated - prefer to use --incompatible_enable_proto_toolchain_resolution.",
            executable = True,
            cfg = "exec",
        ),
    }, **proto_toolchains.if_legacy_toolchain({
        "_legacy_proto_toolchain": attr.label(
            default = "//proto/protobuf:legacy_proto_toolchain",
        ),
    })),
    doc = """\
Declares a Rust Proto toolchain for use.

This is used to configure proto compilation and can be used to set different \
protobuf compiler plugin.

Example:

Suppose a new nicer gRPC plugin has came out. The new plugin can be \
used in Bazel by defining a new toolchain definition and declaration:

```python
load('@rules_rust//proto/protobuf:toolchain.bzl', 'rust_proto_toolchain')

rust_proto_toolchain(
   name="rust_proto_impl",
   grpc_plugin="@rust_grpc//:grpc_plugin",
   grpc_compile_deps=["@rust_grpc//:grpc_deps"],
)

toolchain(
    name="rust_proto",
    exec_compatible_with = [
        "@platforms//cpu:cpuX",
    ],
    target_compatible_with = [
        "@platforms//cpu:cpuX",
    ],
    toolchain = ":rust_proto_impl",
)
```

Then, either add the label of the toolchain rule to register_toolchains in the WORKSPACE, or pass \
it to the `--extra_toolchains` flag for Bazel, and it will be used.

See @rules_rust//proto:BUILD for examples of defining the toolchain.
""",
)
